<?php

namespace App\Model\Dao;

use App\Model\Enum\ChannelCollectionMchConnStatusEnum;
use App\Model\Enum\ChannelCollectionStatusEnum;
use App\Model\Enum\ChannelInfoStatusEnum;
use App\Utils\CustomUtils;
use App\Concern\BaseDao;
use App\Utils\DateUtils;
use Hyperf\Database\Query\Builder;
use Hyperf\Database\Query\JoinClause;
use Hyperf\DbConnection\Db;
use Hyperf\Utils\ApplicationContext;
use Hyperf\Utils\Arr;
use Redis;
use App\Utils\RedisUtils;
use Hyperf\Utils\Codec\Json;

/**
 * ChannelInfoDao
 * 通道
 */
class ChannelInfoDao extends BaseDao {

    /**
     * 表名常量
     * 联表的时候通过XxxDao::TABLE获取
     */
    const TABLE = "channel_info";

    // 不支持软删除
    protected $softDelete = null;

    /**
     * 父类中通过静态函数获取表名, 因为不能通过静态常量获取, 这个函数是固定写在子类中的
     */
    public static function table (): string {
        return self::TABLE;
    }

    /**
     * 通道已用周期额度
     */
    const KEY_HASH_CYCLE_LIMIT = 'table:'.self::TABLE.':hash:cycleLimit';

    /**
     * 调度下标
     */
    const KEY_DISPATCH = 'table:'.self::TABLE.':key:dispatch';

    /**
     * 通道
     */
    const KEY_CHANNEL = 'table:'.self::TABLE.':key:channel';

    /**
     * 商户通道列表
     */
    const KEY_CHANNEL_MCH_LIST = 'table:'.self::TABLE.':key:channel:mch:list';


    /**
     * 排序
     *
     * @param Builder $builder
     * @param array   $params
     *
     * @return Builder|mixed
     */
    protected function _buildOrder (Builder $builder, $params = []) {

        // 通过前端提交的参数排序
        if (Arr::exists($params, "orderBy")) {

            [$prop, $order] = $params["orderBy"];

            if (in_array($order, ["asc", "desc"])) {

                //switch ($prop) {
                //    case "yieldMonth":
                //        $builder->orderBy("main.yieldMonth", $order);
                //        break;
                //    default:
                //        break;
                //}
            }
        }

        // 如果友其他固定的排序需求, 在这里写, 例如按照时间降序
        $builder->orderBy("createTime", "desc");

        // 追加父类的排序, 父类按照ID升序, 保证排序结果唯一
        return parent::_buildOrder($builder, $params);
    }

    /**
     * 查询条件
     *
     * @param $params
     *
     * @return Builder
     */
    protected function _buildQueryString ($params) {

        // 本表一律命名为main
        $main = sprintf("%s AS main", self::TABLE);

        // 联表表明获取
        //$country = sprintf("%s AS country", CountryDao::TABLE);

        $builder = DB::table($main);

        // 通过 Arr::exists 来判断是否存在该键, isset不准确
        //Arr::exists($params, "factoryName") && $builder->where("main.factoryName", "like", "%{$params['factoryName']}%");
        Arr::exists($params, "createTimeRange") && $builder->whereBetween('main.createTime', $params['createTimeRange']);
        return $builder;
    }

    /**
     * 从缓存读取通道
     *
     * @param $channelNo
     *
     * @return \Hyperf\Database\Model\Model|Builder|object|null
     */
    public function readChannel ($channelNo) {

        // 读取缓存
        $key  = self::KEY_CHANNEL.":{$channelNo}";
        $info = RedisUtils::redisResource()->get($key);
        $info = null;

        if (!$info) {

            $info = $this->getWhere(['channelNo' => $channelNo]);

            if (!!$info && $this->isChannelExcess($info)) {
                $info['status'] = ChannelInfoStatusEnum::AUTO_OFF;
            }

            // 写入缓存
            RedisUtils::redisResource()->set($key, serialize($info), DateUtils::getDayRemainSecond());
        }
        else {
            $info = unserialize($info);
        }

        return $info;
    }

    /**
     * 删除通道缓存
     *
     * @param $channelNo
     */
    public function clearChannel ($channelNo) {

        $key = self::KEY_CHANNEL.":{$channelNo}";
        RedisUtils::redisResource()->del($key);

        // 删除关联的商户通道
        $list = ApplicationContext::getContainer()->get(ChannelMchConnDao::class)->getWhereList([
            'channelNo' => $channelNo,
        ]);

        $list = array_column($list, 'mchNo');
        $list = array_unique($list);

        foreach ($list as $mchNo) {
            $this->clearMchChannelLimitList($mchNo);
        }
    }

    /**
     * 通道是否超额
     *
     * @param $channel
     *
     * @return bool
     */
    public function isChannelExcess ($channel) {

        $channelNo = $channel['channelNo'];

        $cost = [
            'dayCount'    => $this->readChannelCount($channelNo, 'day'),
            'monthCount'  => $this->readChannelCount($channelNo, 'month'),
            'dayAmount'   => $this->readChannelAmount($channelNo, 'day'),
            'monthAmount' => $this->readChannelAmount($channelNo, 'month'),
        ];

        return $this->_isChannelExcess($channel, $cost);
    }

    /**
     * 商户通道是否超额
     *
     * @param $mchChannel
     *
     * @return bool
     */
    public function isMchChannelExcess ($mchChannel) {

        $mchNo     = $mchChannel['mchNo'];
        $channelNo = $mchChannel['channelNo'];

        $cost = [
            'dayCount'    => $this->readMchChannelCount($mchNo, $channelNo, 'day'),
            'monthCount'  => $this->readMchChannelCount($mchNo, $channelNo, 'month'),
            'dayAmount'   => $this->readMchChannelAmount($mchNo, $channelNo, 'day'),
            'monthAmount' => $this->readMchChannelAmount($mchNo, $channelNo, 'month'),
        ];

        return $this->_isChannelExcess($mchChannel, $cost);
    }

    /**
     * 检查通道是否超额度
     *
     * @param array $channel 通道限额
     * @param array $cost    通道消耗额度
     *
     * @return bool
     */
    protected function _isChannelExcess ($channel, $cost) {

        $isExcess = false;

        if (!$this->checkLimit($cost['dayCount'], $channel['dayCountLimit'])) {
            // 日周期/次数
            $isExcess = true;
        }
        else if (!$this->checkLimit($cost['monthCount'], $channel['monthCountLimit'])) {
            // 月周期/次数
            $isExcess = true;
        }
        else if (!$this->checkLimit($cost['dayAmount'], $channel['dayAmountLimit'] * $channel['dayAmountThreshold'] / 100)) {
            // 日周期/金额
            $isExcess = true;
        }
        else if (!$this->checkLimit($cost['monthAmount'], $channel['monthAmountLimit'] * $channel['monthAmountThreshold'] / 100)) {
            // 月周期/金额
            $isExcess = true;
        }

        return $isExcess;
    }

    /**
     * 读取商户终端列表
     *
     * @param $mchNo
     *
     * @return array|mixed|void
     */
    public function readMchChannelLimitList ($mchNo) {

        // 读取缓存
        $key  = self::KEY_CHANNEL_MCH_LIST.":{$mchNo}";
        $list = RedisUtils::redisResource()->get($key);

        if (!$list) {

            // 商户-通道集合关联 <- 通道集合 <- 通道集合-通道关联 <- 商户-通道关联 <- 通道

            $main           = sprintf('%s AS main', ChannelCollectionMchConnDao::TABLE);
            $collection     = sprintf('%s AS collection', ChannelCollectionDao::TABLE);
            $collectionConn = sprintf('%s AS collectionConn', ChannelCollectionConnDao::TABLE);
            $channelMch     = sprintf('%s AS channelMch', ChannelMchConnDao::TABLE);
            $channel        = sprintf('%s AS channel', ChannelInfoDao::TABLE);

            $builder = DB::table($main);
            $builder->join($collection, function (JoinClause $join) {
                $join->on('collection.id', 'main.collectionId');
                $join->where('status', ChannelCollectionStatusEnum::ON);
            });
            $builder->join($collectionConn, 'collectionConn.collectionId', 'collection.id');
            $builder->join($channel, 'channel.channelNo', 'collectionConn.channelNo');

            // 从商户通道关联表,根据商户号筛选,再从结果中按照channelNo左连
            $builder->leftJoinSub(Db::table($channelMch)->where('channelMch.mchNo', $mchNo), 'channelMch', function (JoinClause $join) {
                $join->on('channelMch.channelNo', 'channel.channelNo');
            });

            $builder->where('main.mchNo', $mchNo);

            // $builder->orderBy('channelMch.sort', 'desc');
            $builder->orderBy('main.id', 'asc');

            $builder->selectRaw(join(',', [
                'main.collectionId',
                'main.payType',
                'main.mchNo',

                'collection.collectionName',
                'channel.channelNo',

                'collectionConn.sort',
                'channelMch.status',

                'channelMch.amountMin',
                'channelMch.amountLimit',
                'channelMch.dayCountLimit',
                'channelMch.monthCountLimit',
                'channelMch.dayAmountLimit',
                'channelMch.monthAmountLimit',
                'channelMch.dayAmountThreshold',
                'channelMch.monthAmountThreshold',

                'channel.id AS channelId',
                'channel.channelName',
            ]));

            $list = $builder->get()->toArray();
            $list = array_column($list, null, 'channelNo');

            // 调整通道限额开关
            foreach ($list as $channelNo => $mchChannel) {

                // 读取总通道限额
                $channel = $this->readChannel($channelNo);

                if (!$channel || $channel['status'] != ChannelInfoStatusEnum::ON) {
                    // 通道未开启
                    $mchChannel['status'] = ChannelCollectionMchConnStatusEnum::AUTO_OFF;
                }
                else if ($this->isMchChannelExcess($mchChannel)) {
                    // 商户通道超额
                    $mchChannel['status'] = ChannelCollectionMchConnStatusEnum::AUTO_OFF;
                }

                $list[$channelNo] = $mchChannel;
            }

            // 写入缓存
            $this->writeMchChannelLimitList($mchNo, $list);
        }
        else {
            $list = unserialize($list);
        }

        return $list;
    }

    /**
     * 写入商户终端列表缓存
     *
     * @param $mchNo
     * @param $list
     */
    public function writeMchChannelLimitList ($mchNo, $list) {
        $key = self::KEY_CHANNEL_MCH_LIST.":{$mchNo}";
        RedisUtils::redisResource()->set($key, serialize($list), DateUtils::getDayRemainSecond());
    }

    /**
     * 删除商户终端列表缓存
     *
     * @param $mchNo
     */
    public function clearMchChannelLimitList ($mchNo) {
        $key = self::KEY_CHANNEL_MCH_LIST.":{$mchNo}";
        RedisUtils::redisResource()->del($key);
    }


    /**
     * 读取轮询调度下标
     *
     * @param $mchNo
     *
     * @return int
     */
    public function readDispatchPollingPos ($mchNo) {

        $key = self::KEY_DISPATCH.":polling:{$mchNo}";

        $pos = RedisUtils::redisResource()->get($key);

        if (is_bool($pos) && !$pos) {
            $pos = -1;
        }

        return (int) $pos;
    }

    /**
     * 写入轮询调度下标
     *
     * @param $mchNo
     * @param $pos
     */
    public function writeDispatchPollingPos ($mchNo, $pos) {
        $key = self::KEY_DISPATCH.":polling:{$mchNo}";

        if (is_int($pos)) {
            RedisUtils::redisResource()->set($key, $pos, DateUtils::getDayRemainSecond());
        }

    }


    /**
     * 商户通道已用额度
     *
     * @param string $mchNo
     * @param string $channelNo
     * @param string $cycle day|month
     *
     * @return int
     */
    public function readMchChannelAmount ($mchNo, $channelNo, $cycle) {

        $date = $cycle == 'day' ? date('Ymd') : date('Ym');

        $key = ChannelInfoDao::KEY_HASH_CYCLE_LIMIT.':'.$date;

        $num = RedisUtils::redisResource()->hGet($key, "{$mchNo}:{$channelNo}:currentAmount");

        return empty($num) ? 0 : $num;
    }

    /**
     * 通道已用额度
     *
     * @param string $channelNo
     * @param string $cycle day|month
     *
     * @return int
     */
    public function readChannelAmount ($channelNo, $cycle) {

        $date = $cycle == 'day' ? date('Ymd') : date('Ym');

        $key = ChannelInfoDao::KEY_HASH_CYCLE_LIMIT.':'.$date;

        $num = RedisUtils::redisResource()->hGet($key, "{$channelNo}:currentAmount");

        return empty($num) ? 0 : $num;
    }

    /**
     * 商户通道已用次数
     *
     * @param string $mchNo
     * @param string $channelNo
     * @param string $cycle day|month
     *
     * @return int
     */
    public function readMchChannelCount ($mchNo, $channelNo, $cycle) {

        $date = $cycle == 'day' ? date('Ymd') : date('Ym');

        $key = ChannelInfoDao::KEY_HASH_CYCLE_LIMIT.':'.$date;

        $num = RedisUtils::redisResource()->hGet($key, "{$mchNo}:{$channelNo}:currentCount");

        return empty($num) ? 0 : $num;
    }

    /**
     * 通道已用次数
     *
     * @param string $channelNo
     * @param string $cycle day|month
     *
     * @return int
     */
    public function readChannelCount ($channelNo, $cycle) {

        $date = $cycle == 'day' ? date('Ymd') : date('Ym');

        $key = ChannelInfoDao::KEY_HASH_CYCLE_LIMIT.':'.$date;

        $num = RedisUtils::redisResource()->hGet($key, "{$channelNo}:currentCount");

        return empty($num) ? 0 : $num;
    }

    /**
     * 检测某值是否在限制范围内
     * 如果为零则跳过检查
     *
     * @param int  $current    当前值
     * @param int  $limit      限制值
     * @param bool $allowEqual 允许等于
     *
     * @return bool
     */
    public function checkLimit ($current, $limit, $allowEqual = false) {

        $isAllow = false;

        if ($limit > 0) {
            if ($allowEqual) {
                if ($current <= $limit) {
                    $isAllow = true;
                }
            }
            else {
                if ($current < $limit) {
                    $isAllow = true;
                }
            }
        }
        else {
            $isAllow = true;
        }

        return $isAllow;
    }


    /**
     * @param string $cycle day|month
     *
     * @return false|string
     */
    protected function _buildDateKeyByCycle ($cycle) {
        $date = $cycle == 'day' ? date('Ymd') : date('Ym');
        return $date;
    }

    /**
     * 更新额度:商户侧/通道侧,日周期/月周期,次数/金额
     *
     * @param string $mchNo     商户号
     * @param string $channelNo 通道编号
     * @param int    $amount    金额
     * @param int    $count     次数
     * @param string $cycle     周期 day|month
     */
    public function incrChannelLimitCost ($mchNo, $channelNo, int $amount, int $count, $cycle) {

        $date = $this->_buildDateKeyByCycle($cycle);

        // 商户侧
        RedisUtils::redisResource()->hIncrBy(ChannelInfoDao::KEY_HASH_CYCLE_LIMIT.':'.$date, "{$mchNo}:{$channelNo}:currentCount", $count);
        RedisUtils::redisResource()->hIncrBy(ChannelInfoDao::KEY_HASH_CYCLE_LIMIT.':'.$date, "{$mchNo}:{$channelNo}:currentAmount", $amount);

        // 通道侧
        RedisUtils::redisResource()->hIncrBy(ChannelInfoDao::KEY_HASH_CYCLE_LIMIT.':'.$date, "{$channelNo}:currentCount", $count);
        RedisUtils::redisResource()->hIncrBy(ChannelInfoDao::KEY_HASH_CYCLE_LIMIT.':'.$date, "{$channelNo}:currentAmount", $amount);

        // 限制最小值为 0
        if ($this->readMchChannelCount($mchNo, $channelNo, $cycle) < 0) {
            RedisUtils::redisResource()->hDel(ChannelInfoDao::KEY_HASH_CYCLE_LIMIT.':'.$date, "{$mchNo}:{$channelNo}:currentCount");
        }

        if ($this->readMchChannelAmount($mchNo, $channelNo, $cycle) < 0) {
            RedisUtils::redisResource()->hDel(ChannelInfoDao::KEY_HASH_CYCLE_LIMIT.':'.$date, "{$mchNo}:{$channelNo}:currentAmount");
        }

        if ($this->readChannelCount($channelNo, $cycle) < 0) {
            RedisUtils::redisResource()->hDel(ChannelInfoDao::KEY_HASH_CYCLE_LIMIT.':'.$date, "{$channelNo}:currentCount");
        }

        if ($this->readChannelAmount($channelNo, $cycle) < 0) {
            RedisUtils::redisResource()->hDel(ChannelInfoDao::KEY_HASH_CYCLE_LIMIT.':'.$date, "{$channelNo}:currentAmount");
        }
    }


}
